Skip to content

Reject proposals with duplicate actions in GovernorTimelockCompound#6457

Open
gabrielrondon wants to merge 3 commits intoOpenZeppelin:masterfrom
gabrielrondon:fix/6431-duplicate-actions
Open

Reject proposals with duplicate actions in GovernorTimelockCompound#6457
gabrielrondon wants to merge 3 commits intoOpenZeppelin:masterfrom
gabrielrondon:fix/6431-duplicate-actions

Conversation

@gabrielrondon
Copy link
Copy Markdown

Summary

Fixes #6431

Override _propose in GovernorTimelockCompound to reject proposals containing duplicate actions at creation time. The Compound timelock identifies queued transactions by their hash, so duplicate actions within a single proposal cause the second queueTransaction to fail. This check catches the problem earlier with a clearer error message.

Changes

  • IGovernor.sol: Added GovernorDuplicateProposalAction(uint256 index) error
  • GovernorTimelockCompound.sol: Override _propose with O(n²) pairwise comparison of (target, value, keccak256(calldata)) tuples
  • GovernorTimelockCompoundMock.sol: Added required _propose override to resolve diamond inheritance
  • GovernorTimelockCompound.test.js: Updated existing "duplicate calls" test to expect revert at propose() instead of queue()

Test plan

  • forge build — compiles clean
  • npx hardhat test test/governance/extensions/GovernorTimelockCompound.test.js — all 38 tests passing
  • Pre-commit hooks (prettier, eslint, solhint) passing

This PR was developed with AI assistance (Claude).

…imelockCompound

Prevent proposals with duplicate actions (same target, value, calldata) from
being submitted in GovernorTimelockCompound. The Compound timelock identifies
queued transactions by hash, so duplicate actions within a proposal cause
queueTransaction to fail. This override of _propose catches duplicates at
proposal creation time with a clear GovernorDuplicateProposalAction error,
rather than letting them fail later during queueing.

Closes OpenZeppelin#6431
@gabrielrondon gabrielrondon requested a review from a team as a code owner April 3, 2026 09:20
@changeset-bot
Copy link
Copy Markdown

changeset-bot bot commented Apr 3, 2026

🦋 Changeset detected

Latest commit: 9966ae3

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 1 package
Name Type
openzeppelin-solidity Minor

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai bot commented Apr 3, 2026

Walkthrough

The changes introduce validation to prevent proposals with duplicate actions in the GovernorTimelockCompound contract. A new custom error GovernorDuplicateProposalAction was added to IGovernor to document this constraint. An internal _propose override in GovernorTimelockCompound iterates through proposal actions and computes equality by (target, value, keccak256(calldata)), reverting if duplicates are detected. The mock contract was updated to include the override signature. Tests were modified to verify that proposals with duplicate actions are rejected at proposal creation time rather than during queueing or execution.

Suggested labels

security

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The pull request title clearly summarizes the main change: rejecting proposals with duplicate actions in GovernorTimelockCompound.
Description check ✅ Passed The pull request description is comprehensive and directly related to the changeset, explaining the rationale, changes made, and test status.
Linked Issues check ✅ Passed The pull request successfully implements the core requirement from issue #6431: preventing proposals with duplicate actions by overriding _propose to detect and reject duplicates at proposal creation time.
Out of Scope Changes check ✅ Passed All changes are directly scoped to addressing issue #6431: adding the error definition, implementing duplicate detection logic, updating mocks for inheritance, and adjusting tests accordingly.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@contracts/governance/extensions/GovernorTimelockCompound.sol`:
- Around line 74-85: The nested duplicate-scan in
GovernorTimelockCompound._propose indexes values[i] and calldatas[i] assuming
all arrays match targets.length, which can cause out-of-bounds before the
canonical GovernorInvalidProposalLength check; ensure you validate that
values.length == targets.length and calldatas.length == targets.length (or call
the parent length-check) before running the duplicate loops so the existing
GovernorInvalidProposalLength revert fires first; update _propose to perform the
length validation up-front (or delegate to super._propose length checks) and
keep the duplicate detection afterwards, using the existing
GovernorDuplicateProposalAction and GovernorInvalidProposalLength symbols to
guide behavior.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: 35d79bf0-5f38-439b-a062-717f6950a019

📥 Commits

Reviewing files that changed from the base of the PR and between 9cfdccd and 1a862e7.

📒 Files selected for processing (4)
  • contracts/governance/IGovernor.sol
  • contracts/governance/extensions/GovernorTimelockCompound.sol
  • contracts/mocks/governance/GovernorTimelockCompoundMock.sol
  • test/governance/extensions/GovernorTimelockCompound.test.js

Comment on lines +74 to +85
for (uint256 i = 1; i < targets.length; ++i) {
for (uint256 j = 0; j < i; ++j) {
if (
targets[i] == targets[j] &&
values[i] == values[j] &&
keccak256(calldatas[i]) == keccak256(calldatas[j])
) {
revert GovernorDuplicateProposalAction(i);
}
}
}
return super._propose(targets, values, calldatas, description, proposer);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Preserve canonical proposal-length validation before duplicate scan.

At Line 74, the nested loop indexes values[i]/calldatas[i] using targets.length. With malformed inputs, this can panic (out-of-bounds) before reaching Governor._propose’s GovernorInvalidProposalLength revert.

Proposed fix
 function _propose(
     address[] memory targets,
     uint256[] memory values,
     bytes[] memory calldatas,
     string memory description,
     address proposer
 ) internal virtual override returns (uint256) {
+    if (targets.length != values.length || targets.length != calldatas.length || targets.length == 0) {
+        revert GovernorInvalidProposalLength(targets.length, calldatas.length, values.length);
+    }
+
     for (uint256 i = 1; i < targets.length; ++i) {
         for (uint256 j = 0; j < i; ++j) {
             if (
                 targets[i] == targets[j] &&
                 values[i] == values[j] &&
                 keccak256(calldatas[i]) == keccak256(calldatas[j])
             ) {
                 revert GovernorDuplicateProposalAction(i);
             }
         }
     }
     return super._propose(targets, values, calldatas, description, proposer);
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@contracts/governance/extensions/GovernorTimelockCompound.sol` around lines 74
- 85, The nested duplicate-scan in GovernorTimelockCompound._propose indexes
values[i] and calldatas[i] assuming all arrays match targets.length, which can
cause out-of-bounds before the canonical GovernorInvalidProposalLength check;
ensure you validate that values.length == targets.length and calldatas.length ==
targets.length (or call the parent length-check) before running the duplicate
loops so the existing GovernorInvalidProposalLength revert fires first; update
_propose to perform the length validation up-front (or delegate to
super._propose length checks) and keep the duplicate detection afterwards, using
the existing GovernorDuplicateProposalAction and GovernorInvalidProposalLength
symbols to guide behavior.

Add array length validation at the start of _propose() in
GovernorTimelockCompound, before the duplicate-scan loop. Without this,
mismatched array lengths could cause an out-of-bounds access on values[]
or calldatas[] before the canonical GovernorInvalidProposalLength check
in super._propose() has a chance to fire.
…guards

Add tests covering the early array-length validation in
GovernorTimelockCompound._propose():
- empty proposal reverts with GovernorInvalidProposalLength
- targets/values length mismatch reverts with GovernorInvalidProposalLength
- targets/calldatas length mismatch reverts with GovernorInvalidProposalLength

Also add a changeset entry for this PR.

Signed-off-by: Gabriel Rondon <grondon@gmail.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Prevent proposals with duplicate actions from being submitted in GovernorTimelockCompound

1 participant